Beginning Programming - Part Eleven

Expanding our Game

© 2005, Brad Moore

Liberty Basic Connection

Home

Tip Corner

API Corner

Youth Corner

SpriteByte: Basics

StyleBits 3

Shaped Window

Recursion

Eddie's Lessons

Beginners Programming

Help

Index


Remember our goal - to create a playable Pong game from our simple bouncing ball demo (which we just reviewed). To the this code we will need to add:

  1. Add a paddle
  2. Add User control of the paddle
  3. Detect when the ball collides with the paddle
  4. Detect when the ball misses the paddle and hits the wall

First - The Paddle

In my implementation, the paddle is on the right side. It has both a width and height, as well as a position in the x and y directions. The position of the paddle will be moving, so we will need to define a variable to hold this value. I chose also chose to make the height of the paddle a defined variable, making it easier to change later. These are:


    paddleloc = 85
    paddlesize = 60 

Place these variables in the variable definition section of the program (near the top).

The paddle is drawn in two places. First it is drawn in its starting location in the screen initialization and again it is draw with each update cycle when we execute the code in the [move] section. The second instance of the paddle draw is marginally more complex, as we must first remove the old paddle before we draw the new one.

To draw the original paddle at screen initialization, you can use some code like this:


    'create the paddle at the starting location
    #main.gfx " color white; backcolor red"
    #main.gfx "up; goto 465 ";paddleloc;"; down; boxfilled 475 ";paddleloc+paddlesize

Notice that we use the variables we defined earlier, and that they are OUTSIDE of the quoted graphics command string. You will recall us discussing the fact that variable must appear outside of the quoted text in all graphics or text command strings in order for Liberty Basic to recognize the value. If they are part of the quoted string they will give the value zero. (If this is confusing go back and review the last two installments of this series).

Insert the code above just following this snippet in our program:


Open "Window Title" for Window as #main
    #main "trapclose [quit]"
    #main.gfx "down; fill White; flush"
    #main "font ms_sans_serif 10"

The second part of the paddle display involves updating its image during each movement cycle. To do that we will add code to the [move] subroutine. This code will first blank out the original paddle and then draw a new paddle in its new location. We are not moving the paddle yet, so that new location will be the same as the old location. This code is very similar to the code we added earlier to draw the initial paddle. Insert this code immediately following the [move] code label:


    'process the paddle movement
    oldpaddleloc = paddleloc - 5

    'draw the paddle
    #main.gfx "backcolor white"
    #main.gfx "up; goto 465 ";oldpaddleloc;"; down; boxfilled 475 ";oldpaddleloc+paddlesize+10
    #main.gfx "backcolor red"
    #main.gfx "up; goto 465 ";paddleloc;"; down; boxfilled 475 ";paddleloc+paddlesize    

You can run this code now and see that paddle we added.

User Control -

Adding user control of the paddle is just a bit more complex. I actually had to take a couple stabs at this to get halfway acceptable. Liberty Basic does have the ability to add this functionality with a bit more finesse that I am presenting here, but I desired to keep the code as simple as possible.

One level of complexity is my desire to let the user have three paddle speeds, depending on how frequently they have hit the desired keys controlling movement. This took a little work. Remember that we are simply specifying a place to draw the paddle, so to move it faster in one direction or another, we simply increase the distance the paddle moves in a single cycle. I keep track of this amount (or distance) of movement in a variable called "paddle". It can be zero (at rest) or a positive number (moving down) or a negative number (moving up). It is also initialized at the top of the code - insert this line under the Paddlesize initialization:


    paddle = 0

Lets map out the logic to move the paddle:

There are a couple other details I discovered. For instance, it is a good idea to suspend the timer. Also, the program flow must go from this logic into the [move] logic. That is not shown above. Finally, the actual adjustment to the paddles location are made in the [move] logic which must be supplemented with the following code (the first two lines we have already added):


    'process the paddle movement
    oldpaddleloc = paddleloc - 5
    paddleloc = paddleloc + paddle
    if paddleloc < -10 then paddleloc = -10
    if paddleloc > 190 then paddleloc = 190 

Note that we have place boundry limits on the actual movement of the paddle. It can not go above -10 and below 190. This keeps the paddle in view all the time.

The critical missing ingredient here is detecting the user's key press that triggers a movement. This is done by setting up an event trap. We routinely use event traps with Liberty Basic. The code that looks like this:


    #main "trapclose [quit]"

Is an event trap. It traps the window close event that is triggered by clicking the "X" in the upper right corner of a window. When the event is triggered program execution is directed to the "[quit]" subroutine.

The graphicbox (and graphic window) have additional events that can be trapped, including mouse clicks, mouse moves and keyboard events. The helpfile calls this the "When Event" command. Here is what it says:

print #handle, "when event eventHandler"

This tells the window to process mouse and keyboard events. These events occur when a user clicks, double-clicks, drags, or just moves the mouse inside the graphics window. An event can also be the user pressing a key while the graphics window or graphicbox has the input focus (see the setfocus command, above). This provides a really simple mechanism for controlling flow of a program which uses the graphics window.


The eventHandler can be a valid branch label or the name of a subroutine.


In addition, here are the events that are supported by a graphicbox or graphic window:


Events that can be trapped:


leftButtonDown - the left mouse button is now down

leftButtonUp - the left mouse button has been released

leftButtonMove - the mouse moved while the left button is down

leftButtonDouble - the left button has been double-clicked

rightButtonDown - the right mouse button is now down

rightButtonUp - the right mouse button has been released

rightButtonMove - the mouse moved while the right button is down

rightButtonDouble - the right button has been double-clicked

mouseMove - the mouse moved when no button was down

characterInput - a key was pressed while the graphics window has

input focus (see the setfocus command, above)

The event we will need to trap is the CharacterInput event. This is done by simply specifying the handle of the graphicbox, the event and the event handler. Our code looks like this:


    'trap the character input 
    #main.gfx "when characterInput [fetch]"

The event handler needs to be set as soon as possible after the window in which the event is being trapped for has been opened, before any major processing or timing loops. In the case of our program, place it immediately after the code to draw the initial paddle:


Open "Window Title" for Window as #main
    #main "trapclose [quit]"
    #main.gfx "down; fill White; flush"
    #main "font ms_sans_serif 10"
    'create the paddle at the starting location
    #main.gfx "color white; backcolor red"
    #main.gfx "up; goto 465 ";paddleloc;"; down; boxfilled 475 ";paddleloc+paddlesize
    'trap the character input 
    #main.gfx "when characterInput [fetch]"

As you can see looking at the new line of code, it points to an event handler called "[fetch]". This is a subroutine we will write. Every time there is a character input this handler will be called. We want to use "p" for up and "l" for down. Detecting a key press automatically captures which key has been pressed, placing it into a special Liberty Basic variable: Inkey$. Here is what the helpfile says about it:

Keyboard input can only be trapped in graphics windows or graphicboxes. When a key is pressed, the information is stored in the variable Inkey$


Description:

This special variable holds either a single typed character or multiple characters including a Windows virtual keycode. Notice that because Inkey$ is a variable, it is case sensitive. Remember that at this time, only the graphics window and graphicbox controls can scan for keyboard input.

We need to assign the value of Inkey$ to our own variable right away. For temporary processing I like to use simple variable names - in this case a$. Here is how the event handler starts out:


[fetch]
    'process user input
    a$ = Inkey$
    a$ = upper$(a$)

Notice that we use the UPPER$ function to make the value of a$ uppercase. This way we know the case, and do not need to check both upper and lower case values. This simplifies the code. This leaves simply the detection of which key was pressed and what to do. Add this code to the event handler to process the paddle movement:


    if a$ = "P" then
        'move paddle up
        paddle = -3
        goto [move]
    end if
    
    if a$ = "L" then
        'move paddle down
        paddle = 3
        goto [move]
    end if
    
    paddle = 0
    goto [move]

You can try now if you want. In fact I recommend you do try it. You will see something I saw, and it drove me bananas. What happens when you press "p" and "l". Nothing. How ever I know the code is right. Why isn't it working.

Well the reason is buried in that little snippet in the helpfile which we just read. It says "An event can also be the user pressing a key while the graphics window or graphicbox has the input focus (see the setfocus command, above)." Input focus! Focus (which we have discussed before) is the control or window that the Windows OS is currently accepting input from. A user can change the control that has focus by simply clicking on a control. In our code the user has just clicked the "GO" button, which retains the focus, so we know that the graphicbox does NOT have focus. We will need to force the focus to the graphicbox. This is done with the "SETFOCUS" command as suggested in the helpfile snippet.

I recommend reading up on that command - it can be most useful in some situations. Lets add it to our code. Put the following two lines of code immediately following the "[go]" label:


    'set the focus to the graphicbox
    #main.gfx "setfocus"

Now try your program...

We have not integrated paddle acceleration yet, and before we do that, let us look at detecting whether the ball hits the paddle or the back wall (which is out). To do this we need some simple math. We know where the paddle is (the top left corner) and the size of the paddle. We also know when the ball passes through the paddle's vertical plane (xpos = 466). With this bit of data we can code a couple simple IF-THEN statements to control the rebound of the ball when it hits the paddle. Here is my code to accomplish this:


    'check to see if we should have hit the paddle
    if xloc > 466 then 
        'see if we hit that paddle or missed
        if yloc > paddleloc - 5 and yloc < paddleloc + paddlesize + 5  then
            'we hit the paddle - time to rebound...
            goto [changedirection]
        else
            notice "You missed, game over..."
            goto [quit]
        end if
    end if

This code goes into the "[move]" subroutine. It is hard to describe where, so take a look at that entire code block as we have developed it to this point:


[move]
    'process the paddle movement
    oldpaddleloc = paddleloc - 5
    paddleloc = paddleloc + paddle
    if paddleloc < -10 then paddleloc = -10
    if paddleloc > 190 then paddleloc = 190

    'draw the paddle
    #main.gfx "backcolor white"
    #main.gfx "up; goto 465 ";oldpaddleloc;"; down; boxfilled 475 ";oldpaddleloc+paddlesize+10
    #main.gfx "backcolor red"
    #main.gfx "up; goto 465 ";paddleloc;"; down; boxfilled 475 ";paddleloc+paddlesize    

    #main.st1 "Speed: " + str$(abs(xchg))
    #main.st3 "Y-change: " + str$(abs(ychg))
    oldx = xloc
    oldy = yloc
    xloc = xloc + xchg
    yloc = yloc + ychg
    'there is a weird bug that occasionally occurs where y can keep getting smaller
    'or it can keep getting bigger - fix it here
    if yloc < 0 then yloc = 0
    if yloc > 210 then yloc = 210
    #main.gfx "backcolor white"
    #main.gfx "up; goto ";oldx;" ";int(oldy);"; down; circlefilled 10"
    #main.gfx "backcolor red"
    #main.gfx "up; goto ";xloc;" ";int(yloc);"; down; circlefilled 8"

    'check to see if we should have hit the paddle
    if xloc > 466 then 
        'see if we hit that paddle or missed
        if yloc > paddleloc - 5 and yloc < paddleloc + paddlesize + 5  then
            'we hit the paddle - time to rebound...
            goto [changedirection]
        else
            notice "You missed, game over..."
            goto [quit]
        end if
    end if
            
    if yloc < 2 or yloc > 208 then 
        ychg = ychg * -1
    end if
    if xloc < -2 or xloc > 482 then [changedirection]
    wait

At this point we have accomplished almost all the things we hoped to add to the bouncing ball simulation to turn it into a simple, but fun game of PONG. We outlined these things earlier as:

  1. Add a paddle
  2. Add User control of the paddle
  3. Detect when the ball collides with the paddle
  4. Detect when the ball misses the paddle and hits the wall

Even though it looks like we have incorporated all of these into our program, you may remember an element of the User Control that we left out earlier. Acceleration of the paddle. The code has been structured so that this can be added with a minimal effort. That is because the speed of the paddle is controlled by the variable "paddle". Increasing (or decreasing) the value assigned to this variable will have the effect of speeding up the paddle.

I had to experiment with this process a bit as I developed it. I would hate for you (as new programmers) to think that this came out of my head ready to use. It took several tries before I had something that looked and felt good.

The key to making this acceleration work is knowing what key the user last pressed. If for instance the press a "P" and then press "P" again, I want the paddle to travel upward a bit faster. If however they press "P" and then press "L", I do not want to accelerate at all. In fact in this condition I have decided to halt the movement of the paddle. To track the last key pressed, I need variable. I simply decided to use b$. Since this is a permanent variable, I should have used a more descriptive variable name. Unfortunately I did not as I developed this - from time to time we make these kinds of decisions.

I initialize b$ to "##" so that it has a know value at the beginning of program execution. Likewise, I have found that setting b$ back to "##" each time paddle movement halts seems to alleviate a hesitancy that I designed into the paddle movement. Place this code at the beginning of the program where variables are initialized:


b$ = "##"

Next we will alter the paddle movement code in the "[fetch]" subroutine. It will be more complex since we will be checking to see if we have pressed the current key before; If so, we simply add (or subtract) the movement value to (from) the variable "paddle". In addition, we must test to insure that the value of paddle does not get to be too big. Nine (or negative nine) is my upper limit for paddle movement. Here is the modified version of the subroutine.


[fetch]
    'process user input
    a$ = Inkey$
    a$ = upper$(a$)
    
    'when program begins, b$ is preset to default "##"
    if b$ = "##" then b$ = a$
    'if the last keypress was P and this is P then increase down movement
    if a$ = "P" and b$ = "P" then
        'move paddle up
        paddle = paddle - 3
        if paddle < -9 then paddle = -9
        b$ = a$
        goto [move]
    end if
    
    'if the last keypress was L and this is L then increase the upward movement
    if a$ = "L" and b$ = "L" then
        'move paddle down
        paddle = paddle + 3
        if paddle > 9 then paddle = 9
        b$ = a$
        goto [move]
    end if
    
    'This keypress is not the same as the last - stop the paddle
    paddle = 0
    b$ = "##"
    goto [move] 

Another nice change to make would be to add some code to allow us to restart the game without the game ending. This will require that we reset the starting variables and clear the game display, drawing everything in the initial starting location.

I have also incorporated these features in what is the finished game. We have really taken this simple example a long ways. It is now a functional game you can play. To see the whole, complete game, take a look at Appendix A where it is listed.

Prev: Introduction | Next: Working with Sprites